<?php
/**
 * The MIT License
 *
 * Copyright (c) <year> <copyright holders>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @author Damiano Ciarla
 * @package core
 * @subpackage plugin
 */
class BXD_Plugin_Manager
{
	const PLUGIN_INI_CONFIG = "config.ini";

	/**
	 * Lista dei plugin caricati
	 *
	 * @var array
	 */
	private $_classList = array();

	/**
	 * Lista dei plugin di default
	 *
	 * @var array
	 */
	private $_defaultPlugin = array
	(
		/**
		 * Plugin per la gestione delle ACL
		 */
		"AclLite" => "acl_lite",
		
		/**
		 * Plugin per la gestione del routing delle URL
		 */
		"Router" => "router",

		/**
		 * Plugin per la registrazione dei log su Database
		 */
		"Log" => "log",

		/**
		 * Plugin per la gestione del modello dei dati
		 */
		"EntityManager" => "entity_manager",

		/**
		 * Plugin per la gestione dei layout dell'applicazione
		 */
		"Layout" => "layout"
	);

	/**
	 * Classe per l'attivazione dei plugin di default di BXD
	 *
	 * @var BXD_Plugin_Class_SystemPlugin
	 */
	private $_systemPlugin;

	public function __construct()
	{
		// Imposto l'autoload per i plugin
		spl_autoload_register(array($this, "_autoloadClass"));

		// Verifico i plugin presenti nell'applicazione
		$this->_checkLoaded();
	}

	public function load($pluginName)
	{
		// Verifico se il plugin è stato già caricato all'avvio
		if(!array_key_exists($pluginName, $this->_classList))
		{
			// Verifico se il plugin è presente in quelli di sistema
			if(array_key_exists($pluginName, $this->_defaultPlugin))
			{
				// Costruisco il path dove sono presenti i plugin di BXD
				$pluginBasePath = str_replace("core", "plugin", Bxd::getInstance()->pathBxd);

				// Cartella dove sono contenuti i file del plugin
				$pluginFolder = $this->_defaultPlugin[$pluginName];

				// Costruisco il path della cartella del plugin
				$pluginPath = $pluginBasePath . '/'. $pluginFolder;

				// Verifico se la cartella esiste
				if(!is_dir($pluginPath))
					throw new BXD_Plugin_Exception(array($pluginFolder, $pluginName), BXD_Plugin_Exception::BXD_PLUGIN_INCORRECT_PLUGIN_FOLDER);

				// Costruisco il path del file di configurazione del plugin
				$pluginConfigFile = $pluginBasePath . '/'. $pluginFolder . '/config.php';

				// Verifico se il file di configurazione del plugin esiste
				if(!file_exists($pluginConfigFile))
					throw new BXD_Plugin_Exception($pluginName, BXD_Plugin_Exception::BXD_PLUGIN_CONFIG_FILE_NOT_FOUND);

				include_once $pluginConfigFile;

				// Creo il nome della classe di configurazione del plugin
				$className = "Plugin_" . $pluginName;

				if(!class_exists($className))
					throw new BXD_Plugin_Exception(array($pluginConfigFile, $className), BXD_Plugin_Exception::CONFIG_FILES_INCORRECT_DECLARATION_CLASS_NAME);

				$configuration = new $className();

				if(!$configuration instanceof BXD_Plugin_Abstract_Config)
					throw new BXD_Plugin_Exception($className, BXD_Plugin_Exception::INVALID_EXTENDS_TYPE_CLASS);

				if(!$configuration instanceof BXD_Plugin_Interface_Config)
					throw new BXD_Plugin_Exception($className, BXD_Plugin_Exception::INVALID_IMPLEMENTS_TYPE_CLASS);

				$configuration->runConfiguration();

				if($configuration->folder === null || $configuration->folder == "")
					throw new BXD_Plugin_Exception("", BXD_Plugin_Exception::FOLDER_PLUGIN_IS_NOT_SPECIFIED);

				// COPIA DELLE CLASSI DEL PLUGIN

				// Cartella dove sono contenuti i plugin all'interno dell'applicazione
				$applicationPluginBasePath = Bxd::getInstance()->pathApplication . '/' . Bxd::getInstance()->folderConf()->folder_plugin;

				if(!is_writable($applicationPluginBasePath))
					throw new BXD_Plugin_Exception($applicationPluginBasePath, BXD_Plugin_Exception::PLUGIN_FOLDER_IS_NOT_WRITABLE);

				// Path della cartella del plugin per l'applicazione
				$applicationPluginPath = $applicationPluginBasePath . '/' . $configuration->folder;

				// Path del file di configurazione per l'applicazione
				$applicationPluginConfigFile = $applicationPluginPath . '/config.php';

				// Creo la cartella del plugin
				mkdir($applicationPluginPath, 0777, true);

				// Copio il file di configurazione del plugin
				copy($pluginConfigFile, $applicationPluginConfigFile);

				if(count($configuration->getClassList()))
				{
					// Creo la cartella dove saranno inserite le classi del plugin
					mkdir($applicationPluginPath . '/class', 0777, true);

					foreach($configuration->getClassList() as $className => $classPath)
					{
						// Pulisco un eventuale slashes iniziale
						$classPath = ltrim($classPath, '/');

						// Recupero eventuali cartelle da creare
						$subFolder = explode("/", $classPath);

						// Verifico se la classe che si sta analizzando è contenuta in una sotto cartella
						if(count($subFolder) > 1)
						{
							// Ricreo il path togliendo il nome del file
							$subFolder = $applicationPluginPath . '/class/' . implode("/", array_slice($subFolder, 0, -1));

							// Creo la nuova cartella che conterrà la classe che si sta analizzando
							if(!is_dir($subFolder))
								mkdir($subFolder, 0777, true);
						}

						// Copio la classe all'interno del plugin
						copy($pluginPath . '/class/' . $classPath, $applicationPluginPath . '/class/' . $classPath);

						// Imposto il nome della classe per l'autoload
						$this->_classList[$pluginName][$className] = $applicationPluginPath . '/class/' . $classPath;
					}
				}

				// Aggiungo il plugin al file di configurazione dei plugin dell'applicazione
				$applicationPluginConf = $applicationPluginBasePath . '/' . self::PLUGIN_INI_CONFIG;

				$fhAppPluginConf = fopen($applicationPluginConf, 'a+');
				fwrite($fhAppPluginConf, "\n");
				fwrite($fhAppPluginConf, "[{$pluginName}]\n");
				fwrite($fhAppPluginConf, "folder = \"{$configuration->folder}\"\n");
				fclose($fhAppPluginConf);

				// COPIA DEI CONTROLLER DEL PLUGIN

				// Creo il path dove sono contenuti i controller all'interno dell'applicazione
				$applicationControllerBasePath = Bxd::getInstance()->pathApplication . '/' . Bxd::getInstance()->folderConf()->folder_controller;

				// Verifico se il plugin ha bisogno di includere controller all'interno dell'applicazione
				if(count($configuration->getControllerList()))
				{
					// Verifico se la cartella è scrivibile
					if(!is_writable($applicationControllerBasePath))
						throw new BXD_Plugin_Exception($applicationControllerBasePath, BXD_Plugin_Exception::CONTROLLER_FOLDER_IS_NOT_WRITABLE);

					// Scansiono tutti i controller da integrare nell'applicazione
					foreach($configuration->getControllerList() as $controller)
					{
						// Controllo che il controller sia presente nella cartella del plugin
						if(!file_exists($pluginPath . '/controller/' . $controller))
							throw new BXD_Plugin_Exception(array($controller, $pluginName), BXD_Plugin_Exception::CONTROLLER_NOT_FOUND_INTO_PLUGIN);

						// Controllo se è già presente lo stesso controller all'interno dell'applicazione
						if(file_exists($applicationControllerBasePath . '/' . $controller))
							throw new BXD_Plugin_Exception($controller, BXD_Plugin_Exception::CONTROLLER_ALREAD_EXISTS);

						// Verifico la presenza di sottocartelle da creare nell'import del controller
						if(strpos($controller, "/") !== false)
						{
							// Ricavo il path della sottocartella
							$subfloder = $applicationControllerBasePath . '/' . implode("/", array_slice(explode("/", $controller), 0, -1));

							// Se la cartella non esiste la creo
							if(!is_dir($subfloder))
								mkdir($subfloder, 0777, true);
						}

						// Copio il controller nell'applicazione
						copy($pluginPath . '/controller/' . $controller, $applicationControllerBasePath . '/' . $controller);
					}
				}

				// COPIA DELLE ACTION DEL PLUGIN

				// Creo il path dove sono contenute le view all'interno dell'applicazione
				$applicationViewBasePath = Bxd::getInstance()->pathApplication . '/' . Bxd::getInstance()->folderConf()->folder_view;

				// Verifico se il plugin ha bisogno di includere view all'interno dell'applicazione
				if(count($configuration->getViewList()))
				{
					// Verifico se la cartella è scrivibile
					if(!is_writable($applicationViewBasePath))
						throw new BXD_Plugin_Exception($applicationViewBasePath, BXD_Plugin_Exception::ACTION_FOLDER_IS_NOT_WRITABLE);

					// Scansiono tutte le view da integrare nell'applicazione
					foreach($configuration->getViewList() as $view)
					{
						// Controllo che la view sia presente nella cartella del plugin
						if(!file_exists($pluginPath . '/view/' . $view))
							throw new BXD_Plugin_Exception(array($view, $pluginName), BXD_Plugin_Exception::ACTION_NOT_FOUND_INTO_PLUGIN);

						// Controllo se è già presente la stessa view all'interno dell'applicazione
						if(file_exists($applicationViewBasePath . '/' . $view))
							throw new BXD_Plugin_Exception($view, BXD_Plugin_Exception::ACTION_ALREAD_EXISTS);

						// Verifico la presenza di sottocartelle da creare nell'import del controller
						if(strpos($view, "/") !== false)
						{
							// Ricavo il path della sottocartella
							$subfloder = $applicationViewBasePath . '/' . implode("/", array_slice(explode("/", $view), 0, -1));

							// Se la cartella non esiste la creo
							if(!is_dir($subfloder))
								mkdir($subfloder, 0777, true);
						}

						// Copio il controller nell'applicazione
						copy($pluginPath . '/view/' . $view, $applicationViewBasePath . '/' . $view);
					}
				}

				// COPIA DEI MEDIA DEL PLUGIN

				// Creo il path dove sono contenute le view all'interno dell'applicazione
				$applicationWebrootBasePath = Bxd::getInstance()->pathApplication . '/webroot';

				// Verifico se il plugin ha bisogno di includere view all'interno dell'applicazione
				if(count($configuration->getMediaList()))
				{
					// Verifico se la cartella è scrivibile
					if(!is_writable($applicationWebrootBasePath))
						throw new BXD_Plugin_Exception($applicationWebrootBasePath, BXD_Plugin_Exception::ACTION_FOLDER_IS_NOT_WRITABLE);

					// Scansiono tutte le view da integrare nell'applicazione
					foreach($configuration->getMediaList() as $media)
					{
						// Controllo che la view sia presente nella cartella del plugin
						if(!file_exists($pluginPath . '/media/' . $media))
							throw new BXD_Plugin_Exception(array($media, $pluginName), BXD_Plugin_Exception::MEDIA_NOT_FOUND_INTO_PLUGIN);

						// Controllo se è già presente la stessa view all'interno dell'applicazione
						if(file_exists($applicationWebrootBasePath . '/' . $media))
							throw new BXD_Plugin_Exception($media, BXD_Plugin_Exception::MEDIA_ALREAD_EXISTS);

						// Verifico la presenza di sottocartelle da creare nell'import del controller
						if(strpos($media, "/") !== false)
						{
							// Ricavo il path della sottocartella
							$subfloder = $applicationWebrootBasePath . '/' . implode("/", array_slice(explode("/", $media), 0, -1));

							// Se la cartella non esiste la creo
							if(!is_dir($subfloder))
								mkdir($subfloder, 0777, true);
						}

						// Copio il controller nell'applicazione
						copy($pluginPath . '/media/' . $media, $applicationWebrootBasePath . '/' . $media);
					}
				}
			}
			else throw new BXD_Plugin_Exception($pluginName, BXD_Plugin_Exception::PLUGIN_NOT_FOUND);
		}
	}

	/**
	 * Metodo che carica in memoria la lista delle classi per l'autoload del plugin
	 * specificato nel parametro del metodo
	 *
	 * @param string $pluginName Nome del plugin da istanziare
	 */
	public function loadPluginClass($pluginName)
	{
		if(!array_key_exists($pluginName, $this->_classList))
			$this->_checkLoaded($pluginName);
	}

	/**
	 * Metodo che ritorna la lista dei plugin di default che possono essere inclusi
	 * nell'applicazione
	 *
	 * @return BXD_Plugin_Class_SystemPlugin
	 */
	public function activeSystemPlugin()
	{
		if ($this->_systemPlugin === null)
			$this->_systemPlugin = new BXD_Plugin_Class_SystemPlugin();

		return $this->_systemPlugin;
	}

	private function _checkLoaded($pluginToLoad = null)
	{
		// Creo il path dove sono contenuti i plugin dell'applicazione
		$pluginBasePath = Bxd::getInstance()->pathApplication . '/' . Bxd::getInstance()->folderConf()->folder_plugin;

		// Se la cartella non esiste viene creata
		if(!is_dir($pluginBasePath))
			mkdir($pluginBasePath, 0777);

		// Creo il path per il file di configurazione dei plugin dell'applicazione
		$confPluginFile = $pluginBasePath . '/' . self::PLUGIN_INI_CONFIG;

		// Verifico se il file esiste. In caso pisitivo viene aperto in lettura altrimenti viene creato
		if(!file_exists($confPluginFile))
		{
			// Verifico se la cartella dei plugin è scrivibile
			if(!is_writable($pluginBasePath))
				throw new BXD_Plugin_Exception(Bxd::getInstance()->folderConf()->folder_plugin, BXD_Plugin_Exception::PLUGIN_FOLDER_IS_NOT_WRITABLE);

			$fh = fopen($confPluginFile, 'w+');
			fclose($fh);
		}
		
		$pluginConfiguration = parse_ini_file($confPluginFile, true);
		
		if($pluginToLoad !== null && array_key_exists($pluginToLoad, $pluginConfiguration))
		{
			$this->_loadPlugin($pluginToLoad, $pluginConfiguration[$pluginToLoad]);
		}
		elseif($pluginToLoad === null)
		{
			foreach($pluginConfiguration as $plugin => $info)
			{
				$this->_loadPlugin($plugin, $info);
			}
		}
	}

	/**
	 * Metodo per il caricamento delle classi del plugin
	 *
	 * @param string $pluginName Nome del plugin
	 * @param array $info Lista delle informazioni del plugin
	 */
	private function _loadPlugin($pluginName, array $info)
	{
		$pluginFolder = $info['folder'];

		// Se nel file di configurazione dei plugin è abilitato l'autoload o se il nome del plugin compare tra quelli impostati per essere caricati
		// all'avvio o se è stato esplicitato di caricare questo plugin proseguo al suo caricamento
		if(Bxd::getInstance()->pluginConf()->autoloadExistPlugin || in_array($pluginName, Bxd::getInstance()->pluginConf()->getPluginToAutoload()) || $pluginToLoad !== null)
		{
			// Creo il path dove sono contenuti i plugin dell'applicazione
			$pluginBasePath = Bxd::getInstance()->pathApplication . '/' . Bxd::getInstance()->folderConf()->folder_plugin;

			// Costruisco il path della cartella che contiene il plguin
			$pluginPath = $pluginBasePath . '/'. $pluginFolder;

			// Verifico se la cartella esiste
			if(!is_dir($pluginPath))
				throw new BXD_Plugin_Exception(array($pluginFolder, $pluginName), BXD_Plugin_Exception::APPLICATION_PLUGIN_INCORRECT_PLUGIN_FOLDER);

			// Costruisco il path per la classe di configurazione del plugin
			$pluginConfigFile = $pluginBasePath . '/'. $pluginFolder . '/config.php';

			// Verifico se il file esiste
			if(!file_exists($pluginConfigFile))
				throw new BXD_Plugin_Exception($pluginName, BXD_Plugin_Exception::APPLICATION_PLUGIN_CONFIG_FILE_NOT_FOUND);

			include_once $pluginConfigFile;

			// Creo il nome della classe di configurazione del plugin
			$className = "Plugin_" . $pluginName;

			// Verifico se il nome della classe è scritto correttamente
			if(!class_exists($className))
				throw new BXD_Plugin_Exception(array($pluginConfigFile, $className), BXD_Plugin_Exception::CONFIG_FILES_INCORRECT_DECLARATION_CLASS_NAME);

			$configuration = new $className();

			if(!$configuration instanceof BXD_Plugin_Abstract_Config)
				throw new BXD_Plugin_Exception($className, BXD_Plugin_Exception::INVALID_EXTENDS_TYPE_CLASS);

			if(!$configuration instanceof BXD_Plugin_Interface_Config)
				throw new BXD_Plugin_Exception($className, BXD_Plugin_Exception::INVALID_IMPLEMENTS_TYPE_CLASS);

			$configuration->runConfiguration();

			// Scansioni tutte le classe dichiarate nella configurazione e le inserisco nell'array
			// della classi su cui l'autoload può far riferimento
			foreach($configuration->getClassList() as $className => $classPath)
				$this->_classList[$pluginName][$className] = $pluginPath . '/class/' . $classPath;
		}
	}

	private function _autoloadClass($className)
	{
		$classFound = null;

		foreach($this->_classList as $pluginClasses)
		{
			if(array_key_exists($className, $pluginClasses))
				$classFound = $pluginClasses[$className];
		}

		if($classFound !== null)
			include_once $classFound;
	}
}
?>
